// Copyright 2003-2005 Arthur van Hoff, Rick Blair
// Licensed under Apache License version 2.0
// Original license LGPL

package javax.jmdns.impl.tasks;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import javax.jmdns.impl.DNSConstants;
import javax.jmdns.impl.DNSOutgoing;
import javax.jmdns.impl.DNSQuestion;
import javax.jmdns.impl.DNSRecord;
import javax.jmdns.impl.DNSState;
import javax.jmdns.impl.JmDNSImpl;
import javax.jmdns.impl.ServiceInfoImpl;

/**
 * The Prober sends three consecutive probes for all service infos that needs
 * probing as well as for the host name. The state of each service info of the
 * host name is advanced, when a probe has been sent for it. When the prober has
 * run three times, it launches an Announcer.
 * <p/>
 * If a conflict during probes occurs, the affected service infos (and affected
 * host name) are taken away from the prober. This eventually causes the prober
 * tho cancel itself.
 */
public class Prober extends TimerTask {

   /**
     * 
     */
   private final JmDNSImpl jmDNSImpl;
   /**
    * The state of the prober.
    */
   DNSState taskState = DNSState.PROBING_1;

   public Prober(JmDNSImpl jmDNSImpl) {
      this.jmDNSImpl = jmDNSImpl;
      // Associate the host name to this, if it needs probing
      if (this.jmDNSImpl.getState() == DNSState.PROBING_1) {
         this.jmDNSImpl.setTask(this);
      }
      // Associate services to this, if they need probing
      synchronized (this.jmDNSImpl) {
         for (Iterator iterator = this.jmDNSImpl.getServices().values().iterator(); iterator.hasNext();) {
            ServiceInfoImpl info = (ServiceInfoImpl) iterator.next();
            if (info.getState() == DNSState.PROBING_1) {
               info.setTask(this);
            }
         }
      }
   }

   public void start(Timer timer) {
      long now = System.currentTimeMillis();
      if (now - this.jmDNSImpl.getLastThrottleIncrement() < DNSConstants.PROBE_THROTTLE_COUNT_INTERVAL) {
         this.jmDNSImpl.setThrottle(this.jmDNSImpl.getThrottle() + 1);
      } else {
         this.jmDNSImpl.setThrottle(1);
      }
      this.jmDNSImpl.setLastThrottleIncrement(now);

      if (this.jmDNSImpl.getState() == DNSState.ANNOUNCED
               && this.jmDNSImpl.getThrottle() < DNSConstants.PROBE_THROTTLE_COUNT) {
         timer.schedule(this, JmDNSImpl.getRandom().nextInt(1 + DNSConstants.PROBE_WAIT_INTERVAL),
                  DNSConstants.PROBE_WAIT_INTERVAL);
      } else {
         timer.schedule(this, DNSConstants.PROBE_CONFLICT_INTERVAL, DNSConstants.PROBE_CONFLICT_INTERVAL);
      }
   }

   public boolean cancel() {
      // Remove association from host name to this
      if (this.jmDNSImpl.getTask() == this) {
         this.jmDNSImpl.setTask(null);
      }

      // Remove associations from services to this
      synchronized (this.jmDNSImpl) {
         for (Iterator i = this.jmDNSImpl.getServices().values().iterator(); i.hasNext();) {
            ServiceInfoImpl info = (ServiceInfoImpl) i.next();
            if (info.getTask() == this) {
               info.setTask(null);
            }
         }
      }

      return super.cancel();
   }

   public void run() {
      synchronized (this.jmDNSImpl.getIoLock()) {
         DNSOutgoing out = null;
         try {
            // send probes for JmDNS itself
            if (this.jmDNSImpl.getState() == taskState && this.jmDNSImpl.getTask() == this) {
               if (out == null) {
                  out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
               }
               out.addQuestion(new DNSQuestion(this.jmDNSImpl.getLocalHost().getName(), DNSConstants.TYPE_ANY,
                        DNSConstants.CLASS_IN));

               this.jmDNSImpl.getLocalHost().addAddressRecords(out, true);
               this.jmDNSImpl.advanceState();
            }
            // send probes for services
            // Defensively copy the services into a local list,
            // to prevent race conditions with methods registerService
            // and unregisterService.
            List list;
            synchronized (this.jmDNSImpl) {
               list = new LinkedList(this.jmDNSImpl.getServices().values());
            }
            for (Iterator i = list.iterator(); i.hasNext();) {
               ServiceInfoImpl info = (ServiceInfoImpl) i.next();

               synchronized (info) {
                  if (info.getState() == taskState && info.getTask() == this) {
                     info.advanceState();
                     if (out == null) {
                        out = new DNSOutgoing(DNSConstants.FLAGS_QR_QUERY);
                        out.addQuestion(new DNSQuestion(info.getQualifiedName(), DNSConstants.TYPE_ANY,
                                 DNSConstants.CLASS_IN));
                     }
                     // the "unique" flag should be not set here because these
                     // answers haven't been proven unique yet
                     // this means the record will not exactly match the
                     // announcement record
                     out.addAuthorativeAnswer(new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV,
                              DNSConstants.CLASS_IN, DNSConstants.DNS_TTL, info.getPriority(), info.getWeight(), info
                                       .getPort(), this.jmDNSImpl.getLocalHost().getName()));
                  }
               }
            }
            if (out != null) {
               this.jmDNSImpl.send(out);
            } else {
               // If we have nothing to send, another timer taskState ahead
               // of us has done the job for us. We can cancel.
               cancel();
               return;
            }
         } catch (Throwable e) {
            this.jmDNSImpl.recover();
         }

         taskState = taskState.advance();
         if (!taskState.isProbing()) {
            cancel();

            this.jmDNSImpl.startAnnouncer();
         }
      }
   }

}